package com.google.code.gaestats;

import com.google.appengine.api.memcache.MemcacheService;
import lombok.Getter;
import static com.google.code.gaestats.StatsObjectifyFactory.ms;
import static com.google.code.gaestats.StatsObjectifyFactory.ofy;
import com.googlecode.objectify.Ref;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Load;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import org.joda.time.DateTime;
import org.joda.time.Interval;

/**
 *
 * @author JaggardM
 */
@Entity
public /*abstract*/ class Counter
{

	@Id
	@Getter
	protected String name;
	@Load
	@Getter
	protected Ref<PerMinuteCounterData> minuteData;
	@Load("Hour")
	@Getter
	protected Ref<PerHourCounterData> hourData;
	protected DateTime lastSuccessfulUpdate;
	protected List<Interval> unavailableTimes;
	static final CounterFactory[] factories = new CounterFactory[]
	{
		new ValuedCounterFactory(),
		new HitCounterFactory()
	};
	static final String COUNTER_NAME_LIST = "cnl";
	static final String COUNTER_NAME_PREFIX = "cnp_";
	static final String COUNTER_NAME_QUANTITY = "cnq";
	static final String COUNTER = "_cntr";
	static final String NUMBER_SEPERATOR = "_num_";
	static final String LAST_UPDATE = "_last";
	static final int MIN_MILLISECONDS_FOR_MINUTE = 50000;
	static final int MAX_REPEAT_COUNT = 5;
	static final int MINUTE = 60000;

	protected /*abstract*/ void updateCounterData(PerMinuteCounterData pmcd, PerHourCounterData phcd, Iterable<Object> data)
	{
		throw new UnsupportedOperationException();
	}

	public Counter()
	{
	}

	protected Counter(String fullName)
	{
		this.name = fullName;
		checkForNameAndUpdate();
	}

	private void checkUnavailability()
	{
		Object lastUpdate = ms().get(getName() + LAST_UPDATE);
		if (lastUpdate == null && unavailableTimes == null)
		{
			//This is the first time we've seen this counter.
			unavailableTimes = new ArrayList<Interval>();
			ms().put(getName() + LAST_UPDATE, System.currentTimeMillis());
			return;
		}
		if (unavailableTimes == null)
		{
			unavailableTimes = new ArrayList<Interval>();
		}
		if (lastUpdate == null)
		{
			unavailableTimes.add(new Interval(lastSuccessfulUpdate, new DateTime()));
		}
		ms().put(getName() + LAST_UPDATE, System.currentTimeMillis());
		ListIterator<Interval> it = unavailableTimes.listIterator();
		Interval ivl;
		while (it.hasNext())
		{
			ivl = it.next();
			if (ivl.getEnd().isBefore(new DateTime().minusYears(1)))
			{
				it.remove();
			}
			else
			{
				break; //Since we're going in order, if this one isn't before a year ago, none of them are.
			}
		}
	}

	private void checkForNameAndUpdate()
	{
		if (GAEStats.counterNames.contains(name))
		{
			return;
		}
		Object list = ms().get(COUNTER_NAME_LIST);
		if (list != null && list instanceof Iterable)
		{
			for (Object nameO : (Iterable) list)
			{
				if (nameO != null && nameO.equals(name))
				{
					return; //Exists already.
				}
			}
		}
		Long id = ms().increment(COUNTER_NAME_QUANTITY, 1L, 1L);
		if (id != null && id > 0)
		{
			ms().put(COUNTER_NAME_PREFIX + id, name);
		}
	}

	public static void updateCounterNameList()
	{
		MemcacheService.IdentifiableValue qtyIV = ms().getIdentifiable(COUNTER_NAME_QUANTITY);
		List<String> toGet = new ArrayList<String>();
		if (qtyIV != null)
		{
			Object qtyO = qtyIV.getValue();
			if (qtyO != null && qtyO instanceof Number)
			{
				long qty = ((Number) qtyO).longValue();
				for (long i = 1; i <= qty; i++)
				//We start from 1 because zero is the starting value and the id used is after the increment call to memcache.
				{
					toGet.add(COUNTER_NAME_PREFIX + i);
				}
				Map<String, Object> pages = ms().getAll(toGet);
				if (ms().putIfUntouched(COUNTER_NAME_QUANTITY, qtyIV, 0))
				{
					//ms().deleteAll(pages.keySet()); //This could potentially delete results written since the line above finished.
					for (int i = 0; i < MAX_REPEAT_COUNT; i++)
					{
						MemcacheService.IdentifiableValue listIV = ms().getIdentifiable(COUNTER_NAME_LIST);
						if (listIV != null)
						{
							Object listO = listIV.getValue();
							if (listO != null && listO instanceof List)
							{
								((List) listO).addAll(pages.values());
								if (ms().putIfUntouched(COUNTER_NAME_LIST, listIV, listO))
								{
									break;
								}
							}
						}
					}
				}
			}
		}
	}

	protected void persistData()
	{
		checkUnavailability();
		MemcacheService.IdentifiableValue qtyIV = ms().getIdentifiable(getName() + COUNTER);
		List<String> toGet = new ArrayList<String>();
		if (qtyIV != null)
		{
			Object qtyO = qtyIV.getValue();
			if (qtyO != null && qtyO instanceof Number)
			{
				long qty = ((Number) qtyO).longValue();
				for (long i = 1; i <= qty; i++)
				//We start from 1 because 1 is the initial value in the increment call to memcache.
				{
					toGet.add(getName() + NUMBER_SEPERATOR + i);
				}
				Map<String, Object> data = ms().getAll(toGet);
				if (ms().putIfUntouched(getName() + COUNTER, qtyIV, 0))
				{
					PerMinuteCounterData pmcd;
					if (minuteData == null)
					{
						pmcd = new PerMinuteCounterData();
					}
					else
					{
						pmcd = minuteData.get();
						if (pmcd == null)
						{
							pmcd = new PerMinuteCounterData();
						}
					}
					PerHourCounterData phcd;
					if (hourData == null)
					{
						phcd = new PerHourCounterData();
					}
					else
					{
						phcd = hourData.get();
						if (phcd == null)
						{
							phcd = new PerHourCounterData();
						}
					}
					updateCounterData(pmcd, phcd, data.values());
					ofy().save(pmcd, phcd, this);
				}
			}
		}
	}
}